home *** CD-ROM | disk | FTP | other *** search
/ Clickx 47 / Clickx 47.iso / assets / software / Miro_Installer.exe / Miro_Downloader.exe / item.pyc (.txt) < prev    next >
Encoding:
Python Compiled Bytecode  |  2008-01-10  |  56.7 KB  |  2,110 lines

  1. # Source Generated with Decompyle++
  2. # File: in.pyc (Python 2.5)
  3.  
  4. from copy import copy
  5. from datetime import datetime, timedelta
  6. from gtcache import gettext as _
  7. from math import ceil
  8. from xhtmltools import unescape, xhtmlify
  9. from xml.sax.saxutils import unescape
  10. from util import checkU, returnsUnicode, checkF, returnsFilename, quoteUnicodeURL, stringify
  11. from platformutils import FilenameType
  12. import locale
  13. import os
  14. import os.path as os
  15. import urllib
  16. import urlparse
  17. import shutil
  18. import traceback
  19. from download_utils import cleanFilename, nextFreeFilename
  20. from feedparser import FeedParserDict
  21. from database import DDBObject, defaultDatabase, ObjectNotFoundError
  22. from database import DatabaseConstraintError
  23. from databasehelper import makeSimpleGetSet
  24. from iconcache import IconCache
  25. from templatehelper import escape, quoteattr
  26. import types
  27. import app
  28. import template
  29. import downloader
  30. import config
  31. import dialogs
  32. import eventloop
  33. import feed
  34. import filters
  35. import menu
  36. import prefs
  37. import resources
  38. import views
  39. import random
  40. import indexes
  41. import util
  42. import adscraper
  43. import autodler
  44. import moviedata
  45. import logging
  46. import platformutils
  47. import filetypes
  48. import searchengines
  49. import fileutil
  50. import imageresize
  51. import license
  52. _charset = locale.getpreferredencoding()
  53.  
  54. class Item(DDBObject):
  55.     '''An item corresponds to a single entry in a feed. It has a single url
  56.     associated with it.
  57.     '''
  58.     SMALL_ICON_SIZE = (108, 81)
  59.     BIG_ICON_SIZE = (226, 170)
  60.     ICON_CACHE_SIZES = [
  61.         SMALL_ICON_SIZE,
  62.         BIG_ICON_SIZE]
  63.     
  64.     def __init__(self, entry, linkNumber = 0, feed_id = None, parent_id = None):
  65.         self.feed_id = feed_id
  66.         self.parent_id = parent_id
  67.         self.isContainerItem = None
  68.         self.isVideo = False
  69.         self.seen = False
  70.         self.autoDownloaded = False
  71.         self.pendingManualDL = False
  72.         self.downloadedTime = None
  73.         self.watchedTime = None
  74.         self.pendingReason = u''
  75.         self.entry = entry
  76.         self.expired = False
  77.         self.keep = False
  78.         self.videoFilename = FilenameType('')
  79.         self.eligibleForAutoDownload = True
  80.         self.duration = None
  81.         self.screenshot = None
  82.         self.resized_screenshots = { }
  83.         self.resumeTime = 0
  84.         self.channelTitle = None
  85.         self.iconCache = IconCache(self)
  86.         self.linkNumber = linkNumber
  87.         self.creationTime = datetime.now()
  88.         self.updateReleaseDate()
  89.         self._initRestore()
  90.         self._lookForFinishedDownloader()
  91.         DDBObject.__init__(self)
  92.         self.splitItem()
  93.  
  94.     
  95.     def onRestore(self):
  96.         if self.iconCache == None:
  97.             self.iconCache = IconCache(self)
  98.         else:
  99.             self.iconCache.dbItem = self
  100.             self.iconCache.requestUpdate()
  101.         if not hasattr(self, 'isContainerItem'):
  102.             self.isContainerItem = None
  103.         
  104.         self._initRestore()
  105.  
  106.     
  107.     def _initRestore(self):
  108.         '''Common code shared between onRestore and __init__.'''
  109.         self.selected = False
  110.         self.active = False
  111.         self.childrenSeen = None
  112.         self.downloader = None
  113.         self.expiring = None
  114.         self.showMoreInfo = False
  115.         self.updating_movie_info = False
  116.  
  117.     
  118.     def _lookForFinishedDownloader(self):
  119.         dler = downloader.lookupDownloader(self.getURL())
  120.         if dler and dler.isFinished():
  121.             self.downloader = dler
  122.             dler.addItem(self)
  123.         
  124.  
  125.     (getSelected, setSelected) = makeSimpleGetSet(u'selected', changeNeedsSave = False)
  126.     (getActive, setActive) = makeSimpleGetSet(u'active', changeNeedsSave = False)
  127.     
  128.     def getSelectedState(self, view):
  129.         currentView = app.controller.selection.itemListSelection.currentView
  130.         if not (self.selected) or view != currentView:
  131.             return u'normal'
  132.         elif not self.active:
  133.             return u'selected-inactive'
  134.         else:
  135.             return u'selected'
  136.  
  137.     getSelectedState = returnsUnicode(getSelectedState)
  138.     
  139.     def toggleShowMoreInfo(self):
  140.         self.showMoreInfo = not (self.showMoreInfo)
  141.         self.signalChange(needsSave = False, needsUpdateXML = True)
  142.  
  143.     
  144.     def getMoreInfoState(self):
  145.         if self.showMoreInfo:
  146.             return u'more-info'
  147.         
  148.         return u''
  149.  
  150.     getMoreInfoState = returnsUnicode(getMoreInfoState)
  151.     
  152.     def findChildVideos(self):
  153.         '''If this item points to a directory, return the set all video files
  154.         under that directory.
  155.         '''
  156.         videos = set()
  157.         filename_root = self.getFilename()
  158.         if os.path.isdir(filename_root):
  159.             for dirpath, dirnames, filenames in os.walk(filename_root):
  160.                 for name in filenames:
  161.                     filename = os.path.join(dirpath, name)
  162.                     if filetypes.isVideoFilename(filename) or filetypes.isAudioFilename(filename):
  163.                         videos.add(filename)
  164.                         continue
  165.                 
  166.             
  167.         
  168.         return videos
  169.  
  170.     
  171.     def findNewChildren(self):
  172.         '''If this feed is a container item, walk through its directory and
  173.         find any new children.  Returns True if it found childern and ran
  174.         signalChange().
  175.         '''
  176.         filename_root = self.getFilename()
  177.         if not self.isContainerItem:
  178.             return False
  179.         
  180.         if self.getState() == 'downloading':
  181.             return False
  182.         
  183.         videos = self.findChildVideos()
  184.         for child in self.getChildren():
  185.             videos.discard(child.getFilename())
  186.         
  187.         for video in videos:
  188.             if not video.startswith(filename_root):
  189.                 raise AssertionError
  190.             offsetPath = video[len(filename_root):]
  191.             if offsetPath[0] == '/':
  192.                 offsetPath = offsetPath[1:]
  193.             
  194.             FileItem(video, parent_id = self.id, offsetPath = offsetPath)
  195.         
  196.         if videos:
  197.             self.signalChange()
  198.             return True
  199.         
  200.         return False
  201.  
  202.     
  203.     def splitItem(self):
  204.         '''returns True if it ran signalChange()'''
  205.         if self.isContainerItem is not None:
  206.             return self.findNewChildren()
  207.         
  208.         if not isinstance(self, FileItem):
  209.             if self.downloader is None or not self.downloader.isFinished():
  210.                 return False
  211.             
  212.         filename_root = self.getFilename()
  213.         if os.path.isdir(filename_root):
  214.             videos = self.findChildVideos()
  215.             if len(videos) > 1:
  216.                 self.isContainerItem = True
  217.                 for video in videos:
  218.                     if not video.startswith(filename_root):
  219.                         raise AssertionError
  220.                     offsetPath = video[len(filename_root):]
  221.                     if offsetPath[0] == '/':
  222.                         offsetPath = offsetPath[1:]
  223.                     
  224.                     FileItem(video, parent_id = self.id, offsetPath = offsetPath)
  225.                 
  226.             elif len(videos) == 1:
  227.                 self.isContainerItem = False
  228.                 for video in videos:
  229.                     if not video.startswith(filename_root):
  230.                         raise AssertionError
  231.                     self.videoFilename = video[len(filename_root):]
  232.                     if self.videoFilename[0] in ('/', '\\'):
  233.                         self.videoFilename = self.videoFilename[1:]
  234.                     
  235.                     self.isVideo = True
  236.                 
  237.             elif not self.getFeedURL().startswith('dtv:directoryfeed'):
  238.                 target_dir = config.get(prefs.NON_VIDEO_DIRECTORY)
  239.                 if not filename_root.startswith(target_dir):
  240.                     if isinstance(self, FileItem):
  241.                         self.migrate(target_dir)
  242.                     else:
  243.                         self.downloader.migrate(target_dir)
  244.                 
  245.             
  246.             self.isContainerItem = False
  247.         else:
  248.             self.isContainerItem = False
  249.             self.videoFilename = FilenameType('')
  250.             self.isVideo = True
  251.         self.signalChange()
  252.         return True
  253.  
  254.     
  255.     def removeFromPlaylists(self):
  256.         itemIDIndex = indexes.playlistsByItemID
  257.         view = views.playlists.filterWithIndex(itemIDIndex, self.getID())
  258.         for playlist in view:
  259.             playlist.removeItem(self)
  260.         
  261.         view = views.playlistFolders.filterWithIndex(itemIDIndex, self.getID())
  262.         for playlist in view:
  263.             playlist.removeItem(self)
  264.         
  265.  
  266.     
  267.     def updateReleaseDate(self):
  268.         
  269.         try:
  270.             self.releaseDateObj = datetime(*self.getFirstVideoEnclosure().updated_parsed[0:7])
  271.         except:
  272.             
  273.             try:
  274.                 self.releaseDateObj = datetime(*self.entry.updated_parsed[0:7])
  275.             self.releaseDateObj = datetime.min
  276.  
  277.  
  278.  
  279.     
  280.     def checkConstraints(self):
  281.         if self.feed_id is not None:
  282.             
  283.             try:
  284.                 obj = self.dd.getObjectByID(self.feed_id)
  285.             except ObjectNotFoundError:
  286.                 raise DatabaseConstraintError('my feed (%s) is not in database' % self.feed_id)
  287.  
  288.             if not isinstance(obj, feed.Feed):
  289.                 msg = 'feed_id points to a %s instance' % obj.__class__
  290.                 raise DatabaseConstraintError(msg)
  291.             
  292.         
  293.         if self.parent_id is not None:
  294.             
  295.             try:
  296.                 obj = self.dd.getObjectByID(self.parent_id)
  297.             except ObjectNotFoundError:
  298.                 raise DatabaseConstraintError('my parent (%s) is not in database' % self.parent_id)
  299.  
  300.             if not isinstance(obj, Item):
  301.                 msg = 'parent_id points to a %s instance' % obj.__class__
  302.                 raise DatabaseConstraintError(msg)
  303.             
  304.             if obj.isContainerItem is not None and not (obj.isContainerItem):
  305.                 msg = 'parent_id is not a containerItem'
  306.                 raise DatabaseConstraintError(msg)
  307.             
  308.         
  309.         if self.parent_id is None and self.feed_id is None:
  310.             raise DatabaseConstraintError('feed_id and parent_id both None')
  311.         
  312.         if self.parent_id is not None and self.feed_id is not None:
  313.             raise DatabaseConstraintError('feed_id and parent_id both not None')
  314.         
  315.  
  316.     
  317.     def signalChange(self, needsSave = True, needsUpdateXML = True):
  318.         self.expiring = None
  319.         
  320.         try:
  321.             del self._state
  322.         except:
  323.             pass
  324.  
  325.         
  326.         try:
  327.             del self._size
  328.         except:
  329.             pass
  330.  
  331.         if needsUpdateXML:
  332.             
  333.             try:
  334.                 del self._itemXML
  335.  
  336.         
  337.         DDBObject.signalChange(self, needsSave = needsSave)
  338.  
  339.     
  340.     def getItemXML(self, viewName):
  341.         
  342.         try:
  343.             xml = self._itemXML
  344.         except AttributeError:
  345.             self._calcItemXML()
  346.             xml = self._itemXML
  347.  
  348.         return xml.replace(self._XMLViewName, viewName)
  349.  
  350.     
  351.     def _calcItemXML(self):
  352.         self._XMLViewName = 'view%dview' % random.randint(9999999, 99999999)
  353.         self._itemXML = template.fillStaticTemplate('download-item-inner', onlyBody = True, this = self, viewName = self._XMLViewName, templateState = 'unknown')
  354.         checkU(self._itemXML)
  355.  
  356.     
  357.     def getViewed(self):
  358.         
  359.         try:
  360.             return self._feed.lastViewed >= self.creationTime
  361.         except:
  362.             return self.creationTime <= self.getFeed().lastViewed
  363.  
  364.  
  365.     
  366.     def getFirstVideoEnclosure(self):
  367.         
  368.         try:
  369.             return self._firstVidEnc
  370.         except:
  371.             self._calcFirstEnc()
  372.             return self._firstVidEnc
  373.  
  374.  
  375.     
  376.     def _calcFirstEnc(self):
  377.         self._firstVidEnc = getFirstVideoEnclosure(self.entry)
  378.  
  379.     
  380.     def getFirstVideoEnclosureType(self):
  381.         enclosure = self.getFirstVideoEnclosure()
  382.         if enclosure and enclosure.has_key('type'):
  383.             return enclosure['type']
  384.         
  385.  
  386.     getFirstVideoEnclosureType = returnsUnicode(getFirstVideoEnclosureType)
  387.     
  388.     def getURL(self):
  389.         self.confirmDBThread()
  390.         videoEnclosure = self.getFirstVideoEnclosure()
  391.         if videoEnclosure is not None and 'url' in videoEnclosure:
  392.             return quoteUnicodeURL(videoEnclosure['url'].replace('+', '%20'))
  393.         else:
  394.             return u''
  395.  
  396.     getURL = returnsUnicode(getURL)
  397.     
  398.     def getQuotedURL(self):
  399.         return urllib.quote_plus(urllib.unquote(self.getURL().encode('ascii'))).decode('ascii')
  400.  
  401.     getQuotedURL = returnsUnicode(getQuotedURL)
  402.     
  403.     def hasSharableURL(self):
  404.         '''Does this item have a URL that the user can share with others?
  405.  
  406.         This returns True when the item has a non-file URL.
  407.         '''
  408.         url = self.getURL()
  409.         if url != u'':
  410.             pass
  411.         return not url.startswith(u'file:')
  412.  
  413.     
  414.     def getFeed(self):
  415.         
  416.         try:
  417.             return self._feed
  418.         except:
  419.             if self.feed_id is not None:
  420.                 self._feed = self.dd.getObjectByID(self.feed_id)
  421.             elif self.parent_id is not None:
  422.                 self._feed = self.getParent().getFeed()
  423.             else:
  424.                 self._feed = None
  425.             return self._feed
  426.  
  427.  
  428.     
  429.     def getParent(self):
  430.         
  431.         try:
  432.             return self._parent
  433.         except:
  434.             if self.parent_id is not None:
  435.                 self._parent = self.dd.getObjectByID(self.parent_id)
  436.             else:
  437.                 self._parent = self
  438.             return self._parent
  439.  
  440.  
  441.     
  442.     def getFeedURL(self):
  443.         return self.getFeed().getURL()
  444.  
  445.     getFeedURL = returnsUnicode(getFeedURL)
  446.     
  447.     def feedExists(self):
  448.         if self.feed_id:
  449.             pass
  450.         return self.dd.idExists(self.feed_id)
  451.  
  452.     
  453.     def getChildren(self):
  454.         if self.isContainerItem:
  455.             return views.items.filterWithIndex(indexes.itemsByParent, self.id)
  456.         else:
  457.             raise ValueError('%s is not a container item' % self)
  458.  
  459.     
  460.     def setFeed(self, feed_id):
  461.         self.feed_id = feed_id
  462.         del self._feed
  463.         if self.isContainerItem:
  464.             for item in self.getChildren():
  465.                 del item._feed
  466.                 item.signalChange()
  467.             
  468.         
  469.         self.signalChange()
  470.  
  471.     
  472.     def executeExpire(self):
  473.         self.confirmDBThread()
  474.         self.removeFromPlaylists()
  475.         UandA = self.getUandA()
  476.         if not self.isExternal():
  477.             self.deleteFiles()
  478.         
  479.         self.expired = True
  480.         if self.isContainerItem:
  481.             for item in self.getChildren():
  482.                 item.remove()
  483.             
  484.         
  485.         self.isContainerItem = None
  486.         self.isVideo = False
  487.         self.videoFilename = FilenameType('')
  488.         self.seen = self.keep = self.pendingManualDL = False
  489.         self.watchedTime = None
  490.         self.duration = None
  491.         if self.screenshot:
  492.             
  493.             try:
  494.                 os.remove(self.screenshot)
  495.  
  496.         
  497.         self.screenshot = None
  498.         if self.isExternal():
  499.             if self.isDownloaded():
  500.                 new_item = FileItem(self.getVideoFilename(), feed_id = self.feed_id, parent_id = self.parent_id, deleted = True)
  501.                 if self.downloader is not None:
  502.                     self.downloader.setDeleteFiles(False)
  503.                 
  504.             
  505.             self.remove()
  506.         else:
  507.             self.signalChange()
  508.  
  509.     
  510.     def expire(self):
  511.         title = _('Removing %s') % os.path.basename(self.getTitle())
  512.         if self.isExternal():
  513.             if self.isContainerItem:
  514.                 description = _('Would you like to delete this folder and all of its videos or just remove its entry from the Library?')
  515.                 button = dialogs.BUTTON_DELETE_FILES
  516.             elif self.isDownloaded():
  517.                 description = _('Would you like to delete this file or just remove its entry from the Library?')
  518.                 button = dialogs.BUTTON_DELETE_FILE
  519.             else:
  520.                 self.executeExpire()
  521.                 return None
  522.             d = dialogs.ThreeChoiceDialog(title, description, dialogs.BUTTON_REMOVE_ENTRY, button, dialogs.BUTTON_CANCEL)
  523.             
  524.             def callback(dialog):
  525.                 if not self.idExists():
  526.                     return None
  527.                 
  528.                 if dialog.choice == button:
  529.                     self.deleteFiles()
  530.                 
  531.                 if dialog.choice in (button, dialogs.BUTTON_REMOVE_ENTRY):
  532.                     self.executeExpire()
  533.                 
  534.  
  535.             d.run(callback)
  536.         elif self.isContainerItem:
  537.             description = _('This item is a folder.  When you remove a folder, any items inside that folder will be deleted.')
  538.             d = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_DELETE_FILES, dialogs.BUTTON_CANCEL)
  539.             
  540.             def callback(dialog):
  541.                 if self.idExists() and dialog.choice == dialogs.BUTTON_DELETE_FILES:
  542.                     self.executeExpire()
  543.                 
  544.  
  545.             d.run(callback)
  546.         else:
  547.             self.executeExpire()
  548.  
  549.     
  550.     def stopUpload(self):
  551.         if self.downloader:
  552.             self.downloader.stopUpload()
  553.         
  554.  
  555.     
  556.     def startUpload(self):
  557.         if self.downloader:
  558.             self.downloader.startUpload()
  559.         
  560.  
  561.     
  562.     def getString(self, when):
  563.         '''Get the expiration time a string to display to the user.'''
  564.         offset = when - datetime.now()
  565.         if offset.days > 0:
  566.             result = _('%d days') % offset.days
  567.         elif offset.seconds > 3600:
  568.             result = _('%d hours') % ceil(offset.seconds / 3600)
  569.         else:
  570.             result = _('%d minutes') % ceil(offset.seconds / 60)
  571.         return result
  572.  
  573.     getString = returnsUnicode(getString)
  574.     
  575.     def getExpirationString(self):
  576.         '''Get the expiration time a string to display to the user.'''
  577.         expireTime = self.getExpirationTime()
  578.         if expireTime is None:
  579.             return u''
  580.         else:
  581.             return _('Expires in %s') % self.getString(expireTime)
  582.  
  583.     getExpirationString = returnsUnicode(getExpirationString)
  584.     
  585.     def getPausedString(self):
  586.         '''Get the expiration time a string to display to the user.'''
  587.         retryTime = None
  588.         if self.downloader:
  589.             if self.downloader.getState() == u'offline':
  590.                 retryTime = self.downloader.status['retryTime']
  591.                 if retryTime is None:
  592.                     return ''
  593.                 else:
  594.                     return _('Will retry in %s') % self.getString(retryTime)
  595.             else:
  596.                 return _('Paused')
  597.         else:
  598.             return u''
  599.  
  600.     getPausedString = returnsUnicode(getPausedString)
  601.     
  602.     def getDragType(self):
  603.         if self.isDownloaded():
  604.             return u'downloadeditem'
  605.         else:
  606.             return u'item'
  607.  
  608.     getDragType = returnsUnicode(getDragType)
  609.     
  610.     def getEmblemCSSClass(self):
  611.         if self.getState() == u'newly-downloaded':
  612.             return u'newly-downloaded'
  613.         elif self.getState() == u'new':
  614.             return u'new'
  615.         else:
  616.             return u''
  617.  
  618.     getEmblemCSSClass = returnsUnicode(getEmblemCSSClass)
  619.     
  620.     def getEmblemCSSString(self):
  621.         if self.getState() == u'newly-downloaded':
  622.             return u'UNWATCHED'
  623.         elif self.getState() == u'new':
  624.             return u'NEW'
  625.         else:
  626.             return u''
  627.  
  628.     getEmblemCSSString = returnsUnicode(getEmblemCSSString)
  629.     
  630.     def getUandA(self):
  631.         '''Get whether this item is new, or newly-downloaded, or neither.'''
  632.         state = self.getState()
  633.         if state == u'new':
  634.             return (0, 1)
  635.         elif state == u'newly-downloaded':
  636.             return (1, 0)
  637.         else:
  638.             return (0, 0)
  639.  
  640.     
  641.     def getExpirationTime(self):
  642.         """Get the time when this item will expire. 
  643.         Returns a datetime object,  or None if it doesn't expire.
  644.         """
  645.         self.confirmDBThread()
  646.         if self.getWatchedTime() is None or not self.isDownloaded():
  647.             return None
  648.         
  649.         ufeed = self.getFeed()
  650.         if (ufeed.expire == u'never' or ufeed.expire == u'system') and config.get(prefs.EXPIRE_AFTER_X_DAYS) <= 0:
  651.             return None
  652.         elif ufeed.expire == u'feed':
  653.             expireTime = ufeed.expireTime
  654.         elif ufeed.expire == u'system':
  655.             expireTime = timedelta(days = config.get(prefs.EXPIRE_AFTER_X_DAYS))
  656.         
  657.         return self.getWatchedTime() + expireTime
  658.  
  659.     
  660.     def getWatchedTime(self):
  661.         if not self.getSeen():
  662.             return None
  663.         
  664.         if self.isContainerItem and self.watchedTime == None:
  665.             self.watchedTime = datetime.min
  666.             for item in self.getChildren():
  667.                 childTime = item.getWatchedTime()
  668.                 if childTime is None:
  669.                     self.watchedTime = None
  670.                     return None
  671.                 
  672.                 if childTime > self.watchedTime:
  673.                     self.watchedTime = childTime
  674.                     continue
  675.             
  676.             self.signalChange()
  677.         
  678.         return self.watchedTime
  679.  
  680.     
  681.     def getExpiring(self):
  682.         if self.expiring is None:
  683.             if not self.getSeen():
  684.                 self.expiring = False
  685.             else:
  686.                 ufeed = self.getFeed()
  687.                 if (self.keep and ufeed.expire == u'never' or ufeed.expire == u'system') and config.get(prefs.EXPIRE_AFTER_X_DAYS) <= 0:
  688.                     self.expiring = False
  689.                 else:
  690.                     self.expiring = True
  691.         
  692.         return self.expiring
  693.  
  694.     
  695.     def getSeen(self):
  696.         self.confirmDBThread()
  697.         if self.isContainerItem:
  698.             if self.childrenSeen is None:
  699.                 self.childrenSeen = True
  700.                 for item in self.getChildren():
  701.                     if not item.seen:
  702.                         self.childrenSeen = False
  703.                         break
  704.                         continue
  705.                 
  706.             
  707.             return self.childrenSeen
  708.         else:
  709.             return self.seen
  710.  
  711.     
  712.     def markItemSeen(self):
  713.         self.confirmDBThread()
  714.         if self.seen == False:
  715.             self.seen = True
  716.             if self.watchedTime is None:
  717.                 self.watchedTime = datetime.now()
  718.             
  719.             self.clearParentsChildrenSeen()
  720.             self.signalChange()
  721.         
  722.  
  723.     
  724.     def clearParentsChildrenSeen(self):
  725.         if self.parent_id:
  726.             parent = self.getParent()
  727.             parent.childrenSeen = None
  728.             parent.signalChange()
  729.         
  730.  
  731.     
  732.     def markItemUnseen(self):
  733.         self.confirmDBThread()
  734.         if self.isContainerItem:
  735.             self.childrenSeen = False
  736.             for item in self.getChildren():
  737.                 item.seen = False
  738.                 item.signalChange()
  739.             
  740.             self.signalChange()
  741.         elif self.seen == False:
  742.             return None
  743.         
  744.         self.seen = False
  745.         self.watchedTime = None
  746.         self.clearParentsChildrenSeen()
  747.         self.signalChange()
  748.  
  749.     
  750.     def getRSSID(self):
  751.         self.confirmDBThread()
  752.         return self.entry['id']
  753.  
  754.     getRSSID = returnsUnicode(getRSSID)
  755.     
  756.     def removeRSSID(self):
  757.         self.confirmDBThread()
  758.         if 'id' in self.entry:
  759.             del self.entry['id']
  760.             self.signalChange()
  761.         
  762.  
  763.     
  764.     def setAutoDownloaded(self, autodl = True):
  765.         self.confirmDBThread()
  766.         if autodl != self.autoDownloaded:
  767.             self.autoDownloaded = autodl
  768.             self.signalChange()
  769.         
  770.  
  771.     
  772.     def setResumeTime(self, position):
  773.         if not self.idExists():
  774.             return None
  775.         
  776.         position = int(position)
  777.         if self.resumeTime != position:
  778.             self.resumeTime = position
  779.             self.signalChange()
  780.         
  781.  
  782.     setResumeTime = eventloop.asIdle(setResumeTime)
  783.     
  784.     def getPendingReason(self):
  785.         self.confirmDBThread()
  786.         return self.pendingReason
  787.  
  788.     getPendingReason = returnsUnicode(getPendingReason)
  789.     
  790.     def getAutoDownloaded(self):
  791.         self.confirmDBThread()
  792.         return self.autoDownloaded
  793.  
  794.     
  795.     def getLinkNumber(self):
  796.         self.confirmDBThread()
  797.         return self.linkNumber
  798.  
  799.     
  800.     def download(self, autodl = False):
  801.         autodler.resumeDownloader()
  802.         self.confirmDBThread()
  803.         manualDownloadCount = views.manualDownloads.len()
  804.         self.expired = self.keep = self.seen = False
  805.         if not autodl and manualDownloadCount >= config.get(prefs.MAX_MANUAL_DOWNLOADS):
  806.             self.pendingManualDL = True
  807.             self.pendingReason = u'queued for download'
  808.             self.signalChange()
  809.             return None
  810.         else:
  811.             self.setAutoDownloaded(autodl)
  812.             self.pendingManualDL = False
  813.         if self.downloader is None:
  814.             self.downloader = downloader.getDownloader(self)
  815.         
  816.         if self.downloader is not None:
  817.             self.downloader.setChannelName(platformutils.unicodeToFilename(self.getChannelTitle(True)))
  818.             if self.downloader.isFinished():
  819.                 self.onDownloadFinished()
  820.             else:
  821.                 self.downloader.start()
  822.         
  823.         self.signalChange()
  824.  
  825.     
  826.     def pause(self):
  827.         if self.downloader:
  828.             self.downloader.pause()
  829.         
  830.  
  831.     
  832.     def resume(self):
  833.         self.download(self.getAutoDownloaded())
  834.  
  835.     
  836.     def isPendingManualDownload(self):
  837.         self.confirmDBThread()
  838.         return self.pendingManualDL
  839.  
  840.     
  841.     def isEligibleForAutoDownload(self):
  842.         self.confirmDBThread()
  843.         if self.getState() not in (u'new', u'not-downloaded'):
  844.             return False
  845.         
  846.         if self.downloader and self.downloader.getState() in (u'failed', u'stopped', u'paused'):
  847.             return False
  848.         
  849.         ufeed = self.getFeed()
  850.         if ufeed.getEverything:
  851.             return True
  852.         
  853.         return self.eligibleForAutoDownload
  854.  
  855.     
  856.     def isPendingAutoDownload(self):
  857.         if self.getFeed().isAutoDownloadable():
  858.             pass
  859.         return self.isEligibleForAutoDownload()
  860.  
  861.     
  862.     def isFailedDownload(self):
  863.         if self.downloader:
  864.             pass
  865.         return self.downloader.getState() == u'failed'
  866.  
  867.     
  868.     def getThumbnailURL(self):
  869.         self.confirmDBThread()
  870.         videoEnclosure = self.getFirstVideoEnclosure()
  871.         if videoEnclosure is not None:
  872.             
  873.             try:
  874.                 return videoEnclosure['thumbnail']['url'].decode('ascii', 'replace')
  875.  
  876.         
  877.         for enclosure in self.entry.enclosures:
  878.             
  879.             try:
  880.                 return enclosure['thumbnail']['url'].decode('ascii', 'replace')
  881.             continue
  882.             except KeyError:
  883.                 continue
  884.             
  885.  
  886.         
  887.         
  888.         try:
  889.             return self.entry['thumbnail']['url'].decode('ascii', 'replace')
  890.         except:
  891.             None<EXCEPTION MATCH>KeyError
  892.             return None
  893.  
  894.  
  895.     getThumbnailURL = returnsUnicode(getThumbnailURL)
  896.     
  897.     def getThumbnail(self):
  898.         self.confirmDBThread()
  899.         if self.showMoreInfo:
  900.             (width, height) = Item.BIG_ICON_SIZE
  901.         else:
  902.             (width, height) = Item.SMALL_ICON_SIZE
  903.         if self.iconCache.isValid():
  904.             path = self.iconCache.getResizedFilename(width, height)
  905.             return resources.absoluteUrl(path)
  906.         elif self.screenshot:
  907.             path = self.getResizedScreenshot(width, height)
  908.             return resources.absoluteUrl(path)
  909.         elif self.isContainerItem:
  910.             return resources.url(u'images/container-icon.png')
  911.         else:
  912.             feedThumbnail = self.getFeed().getItemThumbnail(width, height)
  913.             if feedThumbnail is not None:
  914.                 return feedThumbnail
  915.             elif self.showMoreInfo:
  916.                 return resources.url(u'images/thumb-more-info.png')
  917.             else:
  918.                 return resources.url(u'images/thumb.png')
  919.  
  920.     getThumbnail = returnsUnicode(getThumbnail)
  921.     
  922.     def getTitle(self):
  923.         
  924.         try:
  925.             return self.entry.title
  926.         except:
  927.             
  928.             try:
  929.                 enclosure = self.getFirstVideoEnclosure()
  930.                 return enclosure['url'].decode('ascii', 'replace')
  931.             return u''
  932.  
  933.  
  934.  
  935.     getTitle = returnsUnicode(getTitle)
  936.     
  937.     def getQuotedTitle(self):
  938.         return urllib.quote_plus(self.getTitle().encode('utf8')).decode('ascii', 'replace')
  939.  
  940.     getQuotedTitle = returnsUnicode(getQuotedTitle)
  941.     
  942.     def setChannelTitle(self, title):
  943.         checkU(title)
  944.         self.channelTitle = title
  945.  
  946.     
  947.     def getChannelTitle(self, allowSearchFeedTitle = False):
  948.         implClass = self.getFeed().actualFeed.__class__
  949.         if implClass in (feed.RSSFeedImpl, feed.ScraperFeedImpl):
  950.             return self.getFeed().getTitle()
  951.         elif implClass == feed.SearchFeedImpl and allowSearchFeedTitle:
  952.             return searchengines.getLastEngineTitle()
  953.         elif self.channelTitle:
  954.             return self.channelTitle
  955.         else:
  956.             return u''
  957.  
  958.     getChannelTitle = returnsUnicode(getChannelTitle)
  959.     
  960.     def getRawDescription(self):
  961.         self.confirmDBThread()
  962.         
  963.         try:
  964.             enclosure = self.getFirstVideoEnclosure()
  965.             return enclosure['text']
  966.         except:
  967.             
  968.             try:
  969.                 return self.entry.description
  970.             return u''
  971.  
  972.  
  973.  
  974.     getRawDescription = returnsUnicode(getRawDescription)
  975.     
  976.     def getDescription(self):
  977.         rawDescription = self.getRawDescription()
  978.         
  979.         try:
  980.             purifiedDescription = adscraper.purify(rawDescription)
  981.             return xhtmlify(u'<span>%s</span>' % (unescape(purifiedDescription),), filterFontTags = True)
  982.         except:
  983.             
  984.             try:
  985.                 return xhtmlify(u'<span>%s</span>' % (unescape(rawDescription),))
  986.             return u'<span />'
  987.  
  988.  
  989.  
  990.     getDescription = returnsUnicode(getDescription)
  991.     
  992.     def getAd(self):
  993.         rawDescription = self.getRawDescription()
  994.         
  995.         try:
  996.             rawAd = adscraper.scrape(rawDescription)
  997.             return xhtmlify(u'<span>%s</span>' % (unescape(rawAd),))
  998.         except:
  999.             return u'<span />'
  1000.  
  1001.  
  1002.     
  1003.     def looksLikeTorrent(self):
  1004.         """Returns true if we think this item is a torrent.  (For items that
  1005.         haven't been downloaded this uses the file extension which isn't
  1006.         totally reliable).
  1007.         """
  1008.         if self.downloader is not None:
  1009.             return self.downloader.getType() == u'bittorrent'
  1010.         else:
  1011.             return self.getURL().endswith(u'.torrent')
  1012.  
  1013.     
  1014.     def getDetails(self):
  1015.         details = []
  1016.         reldate = self.getReleaseDate()
  1017.         format = self.getFormat()
  1018.         size = self.getSizeForDisplay()
  1019.         link = self.getLink()
  1020.         if self.isContainerItem:
  1021.             children = self.getChildren()
  1022.             details.append(u'<span class="details-count">%s items</span>' % len(children))
  1023.         
  1024.         if len(reldate) > 0:
  1025.             details.append(u'<span class="details-date">%s</span>' % escape(reldate))
  1026.         
  1027.         if len(size) > 0:
  1028.             details.append(u'<span class="details-size">%s</span>' % escape(size))
  1029.         
  1030.         if len(format) > 0:
  1031.             details.append(u'<span class="details-format">%s</span>' % escape(format))
  1032.         
  1033.         if self.looksLikeTorrent():
  1034.             details.append(u'<span class="details-torrent">%s</span>' % _('TORRENT'))
  1035.         
  1036.         if len(link) > 0 and link != self.getURL():
  1037.             details.append(u'<a class="details-link" href="%s">%s</span>' % (quoteattr(link), _('WEB PAGE')))
  1038.         
  1039.         out = u'<BR>'.join(details)
  1040.         return out
  1041.  
  1042.     getDetails = returnsUnicode(getDetails)
  1043.     
  1044.     def isTransferring(self):
  1045.         if self.downloader:
  1046.             pass
  1047.         return self.downloader.getState() in (u'uploading', u'downloading')
  1048.  
  1049.     
  1050.     def getDownloadDetails(self):
  1051.         status = self.downloader.status
  1052.         details = [
  1053.             (_('Total Down:'), formatSizeForDetails(status.get('currentSize', 0)))]
  1054.         if status.get('reasonFailed'):
  1055.             details.append((_('Error:'), status['reasonFailed']))
  1056.         
  1057.         return details
  1058.  
  1059.     
  1060.     def getTorrentDetails(self):
  1061.         status = self.downloader.status
  1062.         retval = []
  1063.         seeders = status.get('seeders', -1)
  1064.         leechers = status.get('leechers', -1)
  1065.         if seeders != -1:
  1066.             retval.append((_('Seeders:'), seeders))
  1067.         
  1068.         if leechers != -1:
  1069.             retval.append((_('Leechers:'), leechers))
  1070.         
  1071.         retval.extend([
  1072.             (_('Down Rate:'), formatRateForDetails(status.get('rate', 0))),
  1073.             (_('Down Total:'), formatSizeForDetails(status.get('currentSize', 0))),
  1074.             (_('Up Rate:'), formatRateForDetails(status.get('upRate', 0))),
  1075.             (_('Up Total:'), formatSizeForDetails(status.get('uploaded', 0)))])
  1076.         return retval
  1077.  
  1078.     
  1079.     def getItemDetails(self):
  1080.         rv = []
  1081.         link = self.getLink()
  1082.         if link:
  1083.             rv.append((_('Web page:'), util.makeAnchor(_('permalink'), link)))
  1084.         
  1085.         url = self.getURL()
  1086.         if url and not url.startswith('file:'):
  1087.             rv.append((_('File link:'), util.makeAnchor(_('direct link to file'), url)))
  1088.         
  1089.         rv.append((_('File type:'), self.getFormat()))
  1090.         if self.getLicence():
  1091.             if urlparse.urlparse(self.getLicence())[0]:
  1092.                 ln = license.license_name(self.getLicence())
  1093.                 rv.append((_('License:'), util.makeAnchor(ln, self.getLicence())))
  1094.             else:
  1095.                 rv.append((_('License:'), _('see permalink')))
  1096.         else:
  1097.             rv.append((_('License:'), _('see permalink')))
  1098.         if self.isDownloaded():
  1099.             basename = os.path.basename(self.getFilename())
  1100.             basename = util.clampText(basename, 40)
  1101.             linkEventURL = u'revealItem?item=%d' % self.getID()
  1102.             if self.isContainerItem:
  1103.                 label = _('REVEAL LOCAL FOLDER')
  1104.             else:
  1105.                 label = _('REVEAL LOCAL FILE')
  1106.             link = util.makeEventURL(label, linkEventURL)
  1107.             rv.append((_('Filename:'), u'%s<BR />%s' % (platformutils.filenameToUnicode(basename), link)))
  1108.         
  1109.         return rv
  1110.  
  1111.     
  1112.     def getTorrentDetailsFinished(self):
  1113.         status = self.downloader.status
  1114.         return [
  1115.             (_('Down Total'), formatSizeForDetails(status.get('currentSize', 0))),
  1116.             (_('Up Total'), formatSizeForDetails(status.get('uploaded', 0)))]
  1117.  
  1118.     
  1119.     def makeMoreInfoTable(self, title, moreInfoData):
  1120.         lines = []
  1121.         lines.append(u'<h3>%s</h3>' % title)
  1122.         lines.append(u'<table cellpadding="0" cellspacing="0">')
  1123.         for label, text in moreInfoData:
  1124.             lines.append(u'<tr><td class="label">%s</td><td class="value">%s</td></tr>' % (label, text))
  1125.         
  1126.         lines.append(u'</table>')
  1127.         return u'\n'.join(lines)
  1128.  
  1129.     
  1130.     def getMoreInfo(self):
  1131.         details = [
  1132.             self.makeMoreInfoTable(_('Item Details'), self.getItemDetails())]
  1133.         
  1134.         def addTable(label, data):
  1135.             details.append(self.makeMoreInfoTable(label, data))
  1136.  
  1137.         if self.looksLikeTorrent():
  1138.             if self.isTransferring():
  1139.                 addTable(_('Torrent Details'), self.getTorrentDetails())
  1140.             elif self.downloader and self.downloader.isFinished():
  1141.                 addTable(_('Torrent Details <i>stopped</i>'), self.getTorrentDetailsFinished())
  1142.             
  1143.         elif self.getState() == u'downloading' or not (self.pendingManualDL) or self.isFailedDownload():
  1144.             addTable(_('Download Details'), self.getDownloadDetails())
  1145.         
  1146.         return u'\n'.join(details)
  1147.  
  1148.     getMoreInfo = returnsUnicode(getMoreInfo)
  1149.     
  1150.     def deleteFiles(self):
  1151.         self.confirmDBThread()
  1152.         if self.downloader is not None:
  1153.             self.downloader.removeItem(self)
  1154.             self.downloader = None
  1155.             self.signalChange()
  1156.         
  1157.  
  1158.     
  1159.     def getState(self):
  1160.         '''Get the state of this item.  The state will be on of the following:
  1161.  
  1162.         * new -- User has never seen this item
  1163.         * not-downloaded -- User has seen the item, but not downloaded it
  1164.         * downloading -- Item is currently downloading
  1165.         * newly-downloaded -- Item has been downoladed, but not played
  1166.         * expiring -- Item has been played and is set to expire
  1167.         * saved -- Item has been played and has been saved
  1168.         * expired -- Item has expired.
  1169.  
  1170.         Uses caching to prevent recalculating state over and over
  1171.         '''
  1172.         
  1173.         try:
  1174.             return self._state
  1175.         except AttributeError:
  1176.             self._calcState()
  1177.             return self._state
  1178.  
  1179.  
  1180.     
  1181.     def _calcState(self):
  1182.         self.confirmDBThread()
  1183.         if self.downloader is None or self.downloader.getState() in (u'failed', u'stopped'):
  1184.             if self.pendingManualDL:
  1185.                 self._state = u'downloading'
  1186.             elif self.expired:
  1187.                 self._state = u'expired'
  1188.             elif (self.getViewed() or self.downloader) and self.downloader.getState() in (u'failed', u'stopped'):
  1189.                 self._state = u'not-downloaded'
  1190.             else:
  1191.                 self._state = u'new'
  1192.         elif self.downloader.getState() in (u'offline', u'paused'):
  1193.             if self.pendingManualDL:
  1194.                 self._state = u'downloading'
  1195.             else:
  1196.                 self._state = u'paused'
  1197.         elif not self.downloader.isFinished():
  1198.             self._state = u'downloading'
  1199.         elif not self.getSeen():
  1200.             self._state = u'newly-downloaded'
  1201.         elif self.getExpiring():
  1202.             self._state = u'expiring'
  1203.         else:
  1204.             self._state = u'saved'
  1205.  
  1206.     _calcState = returnsUnicode(_calcState)
  1207.     
  1208.     def getChannelCategory(self):
  1209.         """Get the category to use for the channel template.  
  1210.         
  1211.         This method is similar to getState(), but has some subtle differences.
  1212.         getState() is used by the download-item template and is usually more
  1213.         useful to determine what's actually happening with an item.
  1214.         getChannelCategory() is used by by the channel template to figure out
  1215.         which heading to put an item under.
  1216.  
  1217.         * downloading and not-downloaded are grouped together as
  1218.           not-downloaded
  1219.         * Newly downloaded and downloading items are always new if
  1220.           their feed hasn't been marked as viewed after the item's pub
  1221.           date.  This is so that when a user gets a list of items and
  1222.           starts downloading them, the list doesn't reorder itself.
  1223.           Once they start watching them, then it reorders itself.
  1224.         """
  1225.         self.confirmDBThread()
  1226.         if self.downloader is None or not self.downloader.isFinished():
  1227.             if not self.getViewed():
  1228.                 return u'new'
  1229.             
  1230.             if self.expired:
  1231.                 return u'expired'
  1232.             else:
  1233.                 return u'not-downloaded'
  1234.         elif not self.getSeen():
  1235.             if not self.getViewed():
  1236.                 return u'new'
  1237.             
  1238.             return u'newly-downloaded'
  1239.         elif self.getExpiring():
  1240.             return u'expiring'
  1241.         else:
  1242.             return u'saved'
  1243.  
  1244.     getChannelCategory = returnsUnicode(getChannelCategory)
  1245.     
  1246.     def isDownloadable(self):
  1247.         return self.getState() in (u'new', u'not-downloaded', u'expired')
  1248.  
  1249.     
  1250.     def isDownloaded(self):
  1251.         return self.getState() in (u'newly-downloaded', u'expiring', u'saved')
  1252.  
  1253.     
  1254.     def showSaveButton(self):
  1255.         if self.getState() in (u'newly-downloaded', u'expiring'):
  1256.             pass
  1257.         return not (self.keep)
  1258.  
  1259.     
  1260.     def showSaved(self):
  1261.         if self.getState() in (u'saved',) and self.getState() in (u'newly-downloaded', u'expiring'):
  1262.             pass
  1263.         return self.keep
  1264.  
  1265.     
  1266.     def showTrashButton(self):
  1267.         if self.isDownloaded() and self.getFeedURL() == u'dtv:manualFeed':
  1268.             pass
  1269.         return self.getState() not in (u'downloading', u'paused')
  1270.  
  1271.     
  1272.     def getFailureReason(self):
  1273.         self.confirmDBThread()
  1274.         if self.downloader is not None:
  1275.             return self.downloader.getShortReasonFailed()
  1276.         else:
  1277.             return u''
  1278.  
  1279.     getFailureReason = returnsUnicode(getFailureReason)
  1280.     
  1281.     def getSizeForDisplay(self):
  1282.         return util.formatSizeForUser(self.getSize())
  1283.  
  1284.     
  1285.     def getSize(self):
  1286.         if not hasattr(self, '_size'):
  1287.             self._size = self._getSize()
  1288.         
  1289.         return self._size
  1290.  
  1291.     
  1292.     def _getSize(self):
  1293.         fname = self.getFilename()
  1294.         if self.isDownloaded():
  1295.             
  1296.             try:
  1297.                 return util.getsize(fname)
  1298.             except OSError:
  1299.                 return 0
  1300.             except:
  1301.                 None<EXCEPTION MATCH>OSError
  1302.             
  1303.  
  1304.         None<EXCEPTION MATCH>OSError
  1305.         if self.downloader is not None:
  1306.             return self.downloader.getTotalSize()
  1307.         else:
  1308.             
  1309.             try:
  1310.                 return int(self.getFirstVideoEnclosure()['length'])
  1311.             except:
  1312.                 return 0
  1313.  
  1314.  
  1315.     
  1316.     def getCurrentSize(self):
  1317.         if self.downloader is not None:
  1318.             size = self.downloader.getCurrentSize()
  1319.         else:
  1320.             size = 0
  1321.         return util.formatSizeForUser(size)
  1322.  
  1323.     getCurrentSize = returnsUnicode(getCurrentSize)
  1324.     
  1325.     def downloadProgress(self):
  1326.         progress = 0
  1327.         self.confirmDBThread()
  1328.         if self.downloader is None:
  1329.             return 0
  1330.         else:
  1331.             size = self.downloader.getTotalSize()
  1332.             dled = self.downloader.getCurrentSize()
  1333.             if size == 0:
  1334.                 return 0
  1335.             else:
  1336.                 return 100 * dled / size
  1337.  
  1338.     
  1339.     def gotContentLength(self):
  1340.         if self.downloader is None:
  1341.             return False
  1342.         else:
  1343.             return self.downloader.getTotalSize() != -1
  1344.  
  1345.     
  1346.     def downloadProgressWidth(self):
  1347.         fullWidth = 112
  1348.         progress = self.downloadProgress() / 100
  1349.         if progress == 0:
  1350.             return 0
  1351.         
  1352.         return int(progress * fullWidth)
  1353.  
  1354.     
  1355.     def threeDigitPercentDone(self):
  1356.         return u'%03d' % int(self.downloadProgress())
  1357.  
  1358.     threeDigitPercentDone = returnsUnicode(threeDigitPercentDone)
  1359.     
  1360.     def downloadInProgress(self):
  1361.         if self.downloader is not None:
  1362.             pass
  1363.         return self.downloader.getETA() != 0
  1364.  
  1365.     
  1366.     def downloadETA(self):
  1367.         if self.downloader is not None:
  1368.             totalSecs = self.downloader.getETA()
  1369.             if totalSecs <= 0:
  1370.                 return _('downloading...')
  1371.             
  1372.         else:
  1373.             totalSecs = 0
  1374.         (mins, secs) = divmod(totalSecs, 60)
  1375.         (hours, mins) = divmod(mins, 60)
  1376.         if hours > 0:
  1377.             time = u'%d:%02d:%02d' % (hours, mins, secs)
  1378.             return _('%s remaining') % time
  1379.         else:
  1380.             time = u'%d:%02d' % (mins, secs)
  1381.             return _('%s remaining') % time
  1382.  
  1383.     downloadETA = returnsUnicode(downloadETA)
  1384.     
  1385.     def getStartupActivity(self):
  1386.         if self.pendingManualDL:
  1387.             return self.pendingReason
  1388.         elif self.downloader:
  1389.             return self.downloader.getStartupActivity()
  1390.         else:
  1391.             return _('starting up...')
  1392.  
  1393.     getStartupActivity = returnsUnicode(getStartupActivity)
  1394.     
  1395.     def downloadRate(self):
  1396.         rate = 0
  1397.         unit = u'KB/s'
  1398.         if self.downloader is not None:
  1399.             rate = self.downloader.getRate()
  1400.         else:
  1401.             rate = 0
  1402.         rate /= 1024
  1403.         if rate > 1024:
  1404.             rate /= 1024
  1405.             unit = u'MB/s'
  1406.         
  1407.         if rate > 1024:
  1408.             rate /= 1024
  1409.             unit = u'GB/s'
  1410.         
  1411.         return u'%d%s' % (rate, unit)
  1412.  
  1413.     downloadRate = returnsUnicode(downloadRate)
  1414.     
  1415.     def getPubDate(self):
  1416.         return getReleaseDate()
  1417.  
  1418.     getPubDate = returnsUnicode(getPubDate)
  1419.     
  1420.     def getPubDateParsed(self):
  1421.         return self.getReleaseDateObj()
  1422.  
  1423.     
  1424.     def getReleaseDate(self):
  1425.         
  1426.         try:
  1427.             return self.getReleaseDateObj().strftime('%b %d %Y').decode(_charset)
  1428.         except:
  1429.             return u''
  1430.  
  1431.  
  1432.     getReleaseDate = returnsUnicode(getReleaseDate)
  1433.     
  1434.     def getReleaseDateObj(self):
  1435.         return self.releaseDateObj
  1436.  
  1437.     
  1438.     def getDurationValue(self):
  1439.         secs = 0
  1440.         if self.duration not in (-1, None):
  1441.             secs = self.duration / 1000
  1442.         
  1443.         return secs
  1444.  
  1445.     
  1446.     def getDuration(self, emptyIfZero = True):
  1447.         secs = self.getDurationValue()
  1448.         if secs == 0:
  1449.             if emptyIfZero:
  1450.                 return u''
  1451.             else:
  1452.                 return 'n/a'
  1453.         
  1454.         return u'%02d:%02d' % (secs / 60, secs % 60)
  1455.  
  1456.     getDuration = returnsUnicode(getDuration)
  1457.     KNOWN_MIME_TYPES = (u'audio', u'video')
  1458.     KNOWN_MIME_SUBTYPES = (u'mov', u'wmv', u'mp4', u'mp3', u'mpg', u'mpeg', u'avi', u'x-flv', u'x-msvideo', u'm4v', u'mkv', u'm2v')
  1459.     MIME_SUBSITUTIONS = {
  1460.         u'QUICKTIME': u'MOV' }
  1461.     
  1462.     def getFormat(self, emptyForUnknown = True):
  1463.         if self.looksLikeTorrent():
  1464.             return u'.torrent'
  1465.         
  1466.         
  1467.         try:
  1468.             enclosure = self.entry['enclosures'][0]
  1469.             
  1470.             try:
  1471.                 extension = enclosure['url'].split('.')[-1].lower().decode('ascii', 'replace')
  1472.             except:
  1473.                 extension == u''
  1474.  
  1475.             if extension.lower() == u'mp3':
  1476.                 return u'.mp3'
  1477.             
  1478.             if enclosure.has_key('type') and len(enclosure['type']) > 0:
  1479.                 (mtype, subtype) = enclosure['type'].decode('ascii', 'replace').split('/')
  1480.                 mtype = mtype.lower()
  1481.                 if mtype in self.KNOWN_MIME_TYPES:
  1482.                     format = subtype.split(';')[0].upper()
  1483.                     if mtype == u'audio':
  1484.                         format += u' AUDIO'
  1485.                     
  1486.                     if format.startswith(u'X-'):
  1487.                         format = format[2:]
  1488.                     
  1489.                     return u'.%s' % self.MIME_SUBSITUTIONS.get(format, format).lower()
  1490.                 
  1491.             
  1492.             if extension in self.KNOWN_MIME_SUBTYPES:
  1493.                 return u'.%s' % extension
  1494.         except:
  1495.             pass
  1496.  
  1497.         if emptyForUnknown:
  1498.             return u''
  1499.         else:
  1500.             return u'unknown'
  1501.  
  1502.     getFormat = returnsUnicode(getFormat)
  1503.     
  1504.     def getTags(self):
  1505.         self.confirmDBThread()
  1506.         
  1507.         try:
  1508.             return self.entry.categories.join(u', ')
  1509.         except:
  1510.             return u''
  1511.  
  1512.  
  1513.     getTags = returnsUnicode(getTags)
  1514.     
  1515.     def getLicence(self):
  1516.         self.confirmDBThread()
  1517.         
  1518.         try:
  1519.             return self.entry.license
  1520.         except:
  1521.             
  1522.             try:
  1523.                 return self.getFeed().getLicense()
  1524.             return u''
  1525.  
  1526.  
  1527.  
  1528.     getLicence = returnsUnicode(getLicence)
  1529.     
  1530.     def getPeople(self):
  1531.         ret = []
  1532.         self.confirmDBThread()
  1533.         
  1534.         try:
  1535.             for role in self.getFirstVideoEnclosure().roles:
  1536.                 for person in self.getFirstVideoEnclosure().roles[role]:
  1537.                     ret.append(person)
  1538.                 
  1539.             
  1540.             for role in self.entry.roles:
  1541.                 for person in self.entry.roles[role]:
  1542.                     ret.append(person)
  1543.                 
  1544.         except:
  1545.             pass
  1546.  
  1547.         return u', '.join(ret)
  1548.  
  1549.     getPeople = returnsUnicode(getPeople)
  1550.     
  1551.     def getLink(self):
  1552.         self.confirmDBThread()
  1553.         
  1554.         try:
  1555.             return self.entry.link.decode('ascii', 'replace')
  1556.         except:
  1557.             return u''
  1558.  
  1559.  
  1560.     
  1561.     def getPaymentLink(self):
  1562.         self.confirmDBThread()
  1563.         
  1564.         try:
  1565.             return self.getFirstVideoEnclosure().payment_url.decode('ascii', 'replace')
  1566.         except:
  1567.             
  1568.             try:
  1569.                 return self.entry.payment_url.decode('ascii', 'replace')
  1570.             return u''
  1571.  
  1572.  
  1573.  
  1574.     
  1575.     def getPaymentHTML(self):
  1576.         self.confirmDBThread()
  1577.         
  1578.         try:
  1579.             ret = self.getFirstVideoEnclosure().payment_html
  1580.         except:
  1581.             
  1582.             try:
  1583.                 ret = self.entry.payment_html
  1584.             ret = u''
  1585.  
  1586.  
  1587.         return u'<span>' + unescape(ret) + u'</span>'
  1588.  
  1589.     getPaymentHTML = returnsUnicode(getPaymentHTML)
  1590.     
  1591.     def makeContextMenu(self, templateName, view):
  1592.         c = app.controller
  1593.         if self.isDownloaded():
  1594.             if templateName in ('playlist', 'playlist-folder'):
  1595.                 label = _('Remove From Playlist')
  1596.             else:
  1597.                 label = _('Remove From the Library')
  1598.             items = [
  1599.                 (None, (None, None, None), ((lambda : c.playView(view, self.getID())), _('Play'))),
  1600.                 ((lambda : c.playView(view, self.getID(), True)), _('Play Just This Video')),
  1601.                 (c.addToNewPlaylist, _('Add to new playlist')),
  1602.                 (c.removeCurrentItems, label)]
  1603.             if self.getSeen():
  1604.                 items.append((self.markItemUnseen, _('Mark as Unwatched')))
  1605.             else:
  1606.                 items.append((self.markItemSeen, _('Mark as Watched')))
  1607.             if self.downloader and self.downloader.getState() == 'finished' and self.downloader.getType() == 'bittorrent':
  1608.                 items.append((self.startUpload, _('Restart Upload')))
  1609.             
  1610.         elif self.getState() == 'downloading':
  1611.             items = [
  1612.                 (self.expire, _('Cancel Download')),
  1613.                 (self.pause, _('Pause Download'))]
  1614.         else:
  1615.             items = [
  1616.                 (self.download, _('Download'))]
  1617.         return menu.makeMenu(items)
  1618.  
  1619.     
  1620.     def update(self, entry):
  1621.         UandA = self.getUandA()
  1622.         self.confirmDBThread()
  1623.         
  1624.         try:
  1625.             self.entry = entry
  1626.             self.iconCache.requestUpdate()
  1627.             self.updateReleaseDate()
  1628.             self._calcFirstEnc()
  1629.         finally:
  1630.             self.signalChange()
  1631.  
  1632.  
  1633.     
  1634.     def onDownloadFinished(self):
  1635.         '''Called when the download for this item finishes.'''
  1636.         self.confirmDBThread()
  1637.         self.downloadedTime = datetime.now()
  1638.         if not self.splitItem():
  1639.             self.signalChange()
  1640.         
  1641.         moviedata.movieDataUpdater.requestUpdate(self)
  1642.         for other in views.items:
  1643.             if other.downloader is None and other.getURL() == self.getURL():
  1644.                 other.downloader = self.downloader
  1645.                 self.downloader.addItem(other)
  1646.                 other.signalChange(needsSave = False)
  1647.                 continue
  1648.         
  1649.         app.delegate.notifyDownloadCompleted(self)
  1650.  
  1651.     
  1652.     def getResizedScreenshot(self, width, height):
  1653.         
  1654.         try:
  1655.             return imageresize.getImage(self.resized_screenshots, width, height)
  1656.         except KeyError:
  1657.             return self.screenshot
  1658.  
  1659.  
  1660.     
  1661.     def resizeScreenshot(self):
  1662.         imageresize.removeResizedFiles(self.resized_screenshots)
  1663.         if self.screenshot:
  1664.             self.resized_screenshots = imageresize.multiResizeImage(self.screenshot, self.ICON_CACHE_SIZES)
  1665.         else:
  1666.             self.resized_screenshots = { }
  1667.  
  1668.     
  1669.     def save(self):
  1670.         self.confirmDBThread()
  1671.         if self.keep != True:
  1672.             self.keep = True
  1673.             self.signalChange()
  1674.         
  1675.  
  1676.     
  1677.     def getDownloadedTime(self):
  1678.         if self.downloadedTime is None:
  1679.             return datetime.min
  1680.         else:
  1681.             return self.downloadedTime
  1682.  
  1683.     
  1684.     def getFilename(self):
  1685.         self.confirmDBThread()
  1686.         
  1687.         try:
  1688.             return self.downloader.getFilename()
  1689.         except:
  1690.             return FilenameType('')
  1691.  
  1692.  
  1693.     getFilename = returnsFilename(getFilename)
  1694.     
  1695.     def getVideoFilename(self):
  1696.         self.confirmDBThread()
  1697.         if self.videoFilename:
  1698.             return os.path.join(self.getFilename(), self.videoFilename)
  1699.         else:
  1700.             return self.getFilename()
  1701.  
  1702.     getVideoFilename = returnsFilename(getVideoFilename)
  1703.     
  1704.     def isNonVideoFile(self):
  1705.         if self.isContainerItem != True:
  1706.             pass
  1707.         return not (self.isVideo)
  1708.  
  1709.     
  1710.     def isExternal(self):
  1711.         '''Returns True iff this item was not downloaded from a Democracy
  1712.         channel.
  1713.         '''
  1714.         if self.feed_id is not None:
  1715.             pass
  1716.         return self.getFeedURL() == 'dtv:manualFeed'
  1717.  
  1718.     
  1719.     def isPlayable(self):
  1720.         '''Returns True iff this item should have a play button.'''
  1721.         if not self.isContainerItem:
  1722.             if self.isDownloaded():
  1723.                 pass
  1724.             return self.getVideoFilename()
  1725.         elif self.isDownloaded():
  1726.             pass
  1727.         return len(self.getChildren()) > 0
  1728.  
  1729.     
  1730.     def getRSSEntry(self):
  1731.         self.confirmDBThread()
  1732.         return self.entry
  1733.  
  1734.     
  1735.     def migrateChildren(self, newdir):
  1736.         if self.isContainerItem:
  1737.             for item in self.getChildren():
  1738.                 item.migrate(newdir)
  1739.             
  1740.         
  1741.  
  1742.     
  1743.     def remove(self):
  1744.         if self.downloader is not None:
  1745.             self.downloader.removeItem(self)
  1746.             self.downloader = None
  1747.         
  1748.         if self.iconCache is not None:
  1749.             self.iconCache.remove()
  1750.             self.iconCache = None
  1751.         
  1752.         imageresize.removeResizedFiles(self.resized_screenshots)
  1753.         if self.isContainerItem:
  1754.             for item in self.getChildren():
  1755.                 item.remove()
  1756.             
  1757.         
  1758.         DDBObject.remove(self)
  1759.  
  1760.     
  1761.     def setupLinks(self):
  1762.         """This is called after we restore the database.  Since we don't store
  1763.         references between objects, we need a way to reconnect downloaders to
  1764.         the items after the restore.
  1765.         """
  1766.         if not isinstance(self, FileItem) and self.downloader is None:
  1767.             self.downloader = downloader.getExistingDownloader(self)
  1768.             if self.downloader is not None:
  1769.                 self.signalChange(needsSave = False)
  1770.             
  1771.         
  1772.         self.splitItem()
  1773.         if self.isContainerItem is not None and not os.path.exists(self.getFilename()):
  1774.             self.executeExpire()
  1775.             return None
  1776.         
  1777.         if self.screenshot and not os.path.exists(self.screenshot):
  1778.             self.screenshot = None
  1779.             self.signalChange()
  1780.         
  1781.         if self.duration is None or self.screenshot is None:
  1782.             moviedata.movieDataUpdater.requestUpdate(self)
  1783.         
  1784.  
  1785.     
  1786.     def __str__(self):
  1787.         return 'Item - %s' % self.getTitle()
  1788.  
  1789.  
  1790.  
  1791. def reconnectDownloaders():
  1792.     reconnected = set()
  1793.     for item in views.items:
  1794.         item.setupLinks()
  1795.         reconnected.add(item.downloader)
  1796.     
  1797.     for downloader in views.remoteDownloads:
  1798.         if downloader not in reconnected:
  1799.             logging.warn('removing orphaned downloader: %s', downloader.url)
  1800.             downloader.remove()
  1801.             continue
  1802.     
  1803.     manualFeed = util.getSingletonDDBObject(views.manualFeed)
  1804.     manualItems = views.items.filterWithIndex(indexes.itemsByFeed, manualFeed.getID())
  1805.     for item in manualItems:
  1806.         if item.downloader is None and item.__class__ == Item:
  1807.             logging.warn('removing cancelled external torrent: %s', item)
  1808.             item.remove()
  1809.             continue
  1810.     
  1811.  
  1812.  
  1813. def getEntryForFile(filename):
  1814.     return FeedParserDict({
  1815.         'title': platformutils.filenameToUnicode(os.path.basename(filename)),
  1816.         'enclosures': [
  1817.             {
  1818.                 'url': resources.url(filename) }] })
  1819.  
  1820.  
  1821. def getEntryForURL(url, contentType = None):
  1822.     if contentType is None:
  1823.         contentType = u'video/x-unknown'
  1824.     else:
  1825.         contentType = unicode(contentType)
  1826.     return FeedParserDict({
  1827.         'title': url,
  1828.         'enclosures': [
  1829.             {
  1830.                 'url': url,
  1831.                 'type': contentType }] })
  1832.  
  1833.  
  1834. class FileItem(Item):
  1835.     
  1836.     def __init__(self, filename, feed_id = None, parent_id = None, offsetPath = None, deleted = False):
  1837.         checkF(filename)
  1838.         filename = os.path.abspath(filename)
  1839.         self.filename = filename
  1840.         self.deleted = deleted
  1841.         self.offsetPath = offsetPath
  1842.         self.shortFilename = cleanFilename(os.path.basename(self.filename))
  1843.         Item.__init__(self, getEntryForFile(filename), feed_id = feed_id, parent_id = parent_id)
  1844.         moviedata.movieDataUpdater.requestUpdate(self)
  1845.  
  1846.     
  1847.     def getState(self):
  1848.         if self.deleted:
  1849.             return u'expired'
  1850.         elif self.getSeen():
  1851.             return u'saved'
  1852.         else:
  1853.             return u'newly-downloaded'
  1854.  
  1855.     getState = returnsUnicode(getState)
  1856.     
  1857.     def getChannelCategory(self):
  1858.         """Get the category to use for the channel template.  
  1859.         
  1860.         This method is similar to getState(), but has some subtle differences.
  1861.         getState() is used by the download-item template and is usually more
  1862.         useful to determine what's actually happening with an item.
  1863.         getChannelCategory() is used by by the channel template to figure out
  1864.         which heading to put an item under.
  1865.  
  1866.         * downloading and not-downloaded are grouped together as
  1867.           not-downloaded
  1868.         * Items are always new if their feed hasn't been marked as viewed
  1869.           after the item's pub date.  This is so that when a user gets a list
  1870.           of items and starts downloading them, the list doesn't reorder
  1871.           itself.
  1872.         * Child items match their parents for expiring, where in
  1873.           getState, they always act as not expiring.
  1874.         """
  1875.         self.confirmDBThread()
  1876.         if self.deleted:
  1877.             return u'expired'
  1878.         elif not self.getSeen():
  1879.             return u'newly-downloaded'
  1880.         elif self.parent_id and self.getParent().getExpiring():
  1881.             return u'expiring'
  1882.         else:
  1883.             return u'saved'
  1884.  
  1885.     
  1886.     def getExpiring(self):
  1887.         return False
  1888.  
  1889.     
  1890.     def showSaveButton(self):
  1891.         return False
  1892.  
  1893.     
  1894.     def getViewed(self):
  1895.         return True
  1896.  
  1897.     
  1898.     def isExternal(self):
  1899.         return self.parent_id is None
  1900.  
  1901.     
  1902.     def executeExpire(self):
  1903.         self.confirmDBThread()
  1904.         self.removeFromPlaylists()
  1905.         if self.isContainerItem:
  1906.             for item in self.getChildren():
  1907.                 item.remove()
  1908.             
  1909.         
  1910.         if not os.path.exists(self.filename):
  1911.             self.remove()
  1912.         elif self.feed_id is None:
  1913.             self.deleted = True
  1914.             self.signalChange()
  1915.         else:
  1916.             url = self.getFeedURL()
  1917.             if url.startswith('dtv:manualFeed') or url.startswith('dtv:singleFeed'):
  1918.                 self.remove()
  1919.             else:
  1920.                 self.deleted = True
  1921.                 self.signalChange()
  1922.  
  1923.     
  1924.     def deleteFiles(self):
  1925.         
  1926.         try:
  1927.             if self.getParent():
  1928.                 dler = self.getParent().downloader
  1929.                 if dler:
  1930.                     dler.stop(False)
  1931.                 
  1932.             
  1933.             if os.path.isfile(self.filename):
  1934.                 os.remove(self.filename)
  1935.             elif os.path.isdir(self.filename):
  1936.                 shutil.rmtree(self.filename)
  1937.         except:
  1938.             logging.warn('WARNING: error deleting files:\n%s', traceback.format_exc())
  1939.  
  1940.  
  1941.     
  1942.     def getDownloadedTime(self):
  1943.         self.confirmDBThread()
  1944.         
  1945.         try:
  1946.             return datetime.fromtimestamp(os.path.getctime(self.filename))
  1947.         except:
  1948.             return datetime.min
  1949.  
  1950.  
  1951.     
  1952.     def getFilename(self):
  1953.         
  1954.         try:
  1955.             return self.filename
  1956.         except:
  1957.             return FilenameType('')
  1958.  
  1959.  
  1960.     getFilename = returnsFilename(getFilename)
  1961.     
  1962.     def download(self, autodl = False):
  1963.         self.deleted = False
  1964.         self.signalChange()
  1965.  
  1966.     
  1967.     def updateReleaseDate(self):
  1968.         
  1969.         try:
  1970.             self.releaseDateObj = datetime.fromtimestamp(os.path.getmtime(self.filename))
  1971.         except:
  1972.             self.releaseDateObj = datetime.min
  1973.  
  1974.  
  1975.     
  1976.     def getReleaseDateObj(self):
  1977.         if self.parent_id:
  1978.             return self.getParent().releaseDateObj
  1979.         else:
  1980.             return self.releaseDateObj
  1981.  
  1982.     
  1983.     def migrate(self, newDir):
  1984.         self.confirmDBThread()
  1985.         if self.parent_id:
  1986.             parent = self.getParent()
  1987.             self.filename = os.path.join(parent.getFilename(), self.offsetPath)
  1988.             return None
  1989.         
  1990.         if self.shortFilename is None:
  1991.             logging.warn("can't migrate download because we don't have a shortFilename!\nfilename was %s", stringify(self.filename))
  1992.             return None
  1993.         
  1994.         newFilename = os.path.join(newDir, self.shortFilename)
  1995.         if self.filename == newFilename:
  1996.             return None
  1997.         
  1998.         if os.path.exists(self.filename):
  1999.             newFilename = nextFreeFilename(newFilename)
  2000.             
  2001.             def callback():
  2002.                 self.filename = newFilename
  2003.                 self.signalChange()
  2004.  
  2005.             fileutil.migrate_file(self.filename, newFilename, callback)
  2006.         elif os.path.exists(newFilename):
  2007.             self.filename = newFilename
  2008.             self.signalChange()
  2009.         
  2010.         self.migrateChildren(newDir)
  2011.  
  2012.     
  2013.     def setupLinks(self):
  2014.         if self.shortFilename is None:
  2015.             if self.parent_id is None:
  2016.                 self.shortFilename = cleanFilename(os.path.basename(self.filename))
  2017.             else:
  2018.                 parent_file = self.getParent().getFilename()
  2019.                 if self.filename.startswith(parent_file):
  2020.                     self.shortFilename = cleanFilename(self.filename[len(parent_file):])
  2021.                 else:
  2022.                     logging.warn('%s is not a subdirectory of %s', self.filename, parent_file)
  2023.         
  2024.         self.updateReleaseDate()
  2025.         Item.setupLinks(self)
  2026.  
  2027.  
  2028.  
  2029. def expireItems(items):
  2030.     if len(items) == 1:
  2031.         return items[0].expire()
  2032.     
  2033.     hasContainers = False
  2034.     hasExternalItems = False
  2035.     for item in items:
  2036.         if item.isContainerItem:
  2037.             hasContainers = True
  2038.         elif item.isExternal():
  2039.             hasExternalItems = True
  2040.         
  2041.         if hasContainers and hasExternalItems:
  2042.             break
  2043.             continue
  2044.     
  2045.     title = _('Removing %s items') % len(items)
  2046.     if hasExternalItems:
  2047.         description = _('One or more of these videos was not downloaded from a channel.  Would you like to delete these items or just remove their entries from the Library?')
  2048.     else:
  2049.         description = u'Are you sure you want to delete all %s videos?' % len(items)
  2050.     if hasContainers:
  2051.         description += u'\n\n' + _('One or more of these items is a folder.  When you remove or delete a folder, any items inside that folder will also be removed or deleted.')
  2052.     
  2053.     if hasExternalItems:
  2054.         d = dialogs.ThreeChoiceDialog(title, description, dialogs.BUTTON_REMOVE_ENTRY, dialogs.BUTTON_DELETE_FILES, dialogs.BUTTON_CANCEL)
  2055.     else:
  2056.         d = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_OK, dialogs.BUTTON_CANCEL)
  2057.     
  2058.     def callback(dialog):
  2059.         if dialog.choice == dialogs.BUTTON_DELETE_FILES:
  2060.             for item in items:
  2061.                 if item.idExists() and isinstance(item, FileItem):
  2062.                     item.deleteFiles()
  2063.                     continue
  2064.             
  2065.         
  2066.         if dialog.choice in (dialogs.BUTTON_OK, dialogs.BUTTON_REMOVE_ENTRY, dialogs.BUTTON_DELETE_FILES):
  2067.             for item in items:
  2068.                 if item.idExists():
  2069.                     item.executeExpire()
  2070.                     continue
  2071.             
  2072.         
  2073.  
  2074.     d.run(callback)
  2075.  
  2076.  
  2077. def getFirstVideoEnclosure(entry):
  2078.     '''Find the first video enclosure in a feedparser entry.  Returns the
  2079.     enclosure, or None if no video enclosure is found.
  2080.     '''
  2081.     
  2082.     try:
  2083.         enclosures = entry.enclosures
  2084.     except (KeyError, AttributeError):
  2085.         return None
  2086.  
  2087.     for enclosure in enclosures:
  2088.         if filetypes.isVideoEnclosure(enclosure):
  2089.             return enclosure
  2090.             continue
  2091.     
  2092.     return None
  2093.  
  2094.  
  2095. def formatRateForDetails(bytes):
  2096.     '''Format a download/upload rate for the more-details view.'''
  2097.     sizeFmt = util.formatSizeForUser(bytes, zeroString = u'-')
  2098.     if bytes > 0:
  2099.         return sizeFmt + u'/s'
  2100.     else:
  2101.         return sizeFmt
  2102.  
  2103. formatRateForDetails = returnsUnicode(formatRateForDetails)
  2104.  
  2105. def formatSizeForDetails(bytes):
  2106.     '''Format a disk size for the more-details view.'''
  2107.     return util.formatSizeForUser(bytes, zeroString = u'-')
  2108.  
  2109. formatSizeForDetails = returnsUnicode(formatSizeForDetails)
  2110.